package ferp.android.managers;

import android.app.Activity;
import android.content.Context;
import android.content.IntentSender;
import android.os.AsyncTask;
import android.os.Bundle;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.drive.Drive;
import com.google.android.gms.games.Games;
import com.google.android.gms.games.GamesActivityResultCodes;
import com.google.android.gms.games.GamesStatusCodes;
import com.google.android.gms.games.snapshot.*;
import ferp.core.game.Game;
import ferp.core.game.Settings;
import ferp.core.log.Log;
import ferp.core.player.Profile;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * User: olegs
 * Date: 29/06/2014 11:18
 */
public class CloudManager
{
  public static final int REQUEST_CODE = 1000;

  public static class Listener
  {
    public static interface Login
    {
      void onLoginSuccess();
      void onLoginFail();
    }

    public static interface Load
    {
      void onLoadStart();
      void onLoadEnd(List<Profile> loaded);
    }

    private Login login;
    //private Load  load;
  }

  public static CloudManager instance(Context context)
  {
    if (instance == null)
      instance = new CloudManager(context.getApplicationContext());

    return instance;
  }

  public boolean isEnabled()
  {
    return gac != null;
  }

  public boolean isLoggedIn() {return isEnabled() && gac.isConnected();}

  public void login(Activity activity, Listener.Login listener)
  {
    if (isEnabled())
    {
      this.activity = activity;
      this.listeners.login = listener;

      if (!gac.isConnecting())
      {
        if (!isLoggedIn())
        {
          Log.debug(Log.TAG, "connecting to gms...");

          gac.connect();
        }
        else
        {
          Log.debug(Log.TAG, "already connected to gms");
          gac.reconnect();
        }
      }
      else
        Log.debug(Log.TAG, "already trying to connect to gms");
    }
    else
      Log.debug(Log.TAG, "play services are not available");
  }

  public void load(final Listener.Load listener)
  {
    if (isLoggedIn())
    {
      Log.debug(Log.TAG, "loading game snapshot from cloud");

      new AsyncTask<Void, Void, List<Profile>>()
      {
        @Override
        protected void onPreExecute()
        {
          listener.onLoadStart();
        }

        @Override
        protected List<Profile> doInBackground(Void... voids)
        {
          try
          {
            Snapshots.LoadSnapshotsResult lsr = Games.Snapshots.load(gac, true).await(30, TimeUnit.SECONDS);

            switch (lsr.getStatus().getStatusCode())
            {
              case GamesStatusCodes.STATUS_OK:
                return load(lsr.getSnapshots());

              default:
                Log.debug(Log.TAG, "error loading snapshot list, status " + lsr.getStatus());
            }
          }
          catch (Exception e)
          {
            Log.error(Log.TAG, e);
          }

          return null;
        }

        @Override
        protected void onPostExecute(List<Profile> profiles)
        {
          listener.onLoadEnd(profiles);
        }
      }.execute();
    }
    else
      Log.debug(Log.TAG, "not connected to gms - no load");
  }

  public void save(final Profile profile)
  {
    if (isLoggedIn())
    {
      //final String id = profile.id;
      final byte[] data = Game.gson.toJson(profile).getBytes();

      new AsyncTask<Void, Void, Void>()
      {
        @Override
        protected Void doInBackground(Void... voids)
        {
          Log.debug(Log.TAG, "cloud save for profile id [" + profile.id + "] , name: " + profile.nickname);

          for (int retries = 0; retries < 3; ++retries)
            try
            {
              Snapshots.OpenSnapshotResult osr = Games.Snapshots.open(gac, profile.id, true).await(60, TimeUnit.SECONDS);

              Log.debug(Log.TAG, "cloud save retry #" + retries + ", status " + osr.getStatus());

              switch (osr.getStatus().getStatusCode())
              {
                case GamesStatusCodes.STATUS_SNAPSHOT_CONTENTS_UNAVAILABLE:
                  Log.debug(Log.TAG, "STATUS_SNAPSHOT_CONTENTS_UNAVAILABLE");
                case GamesStatusCodes.STATUS_OK:
                {
                  Snapshot s = osr.getSnapshot();

                  s.writeBytes(data);
                  Games.Snapshots.commitAndClose(gac, s, new SnapshotMetadataChange.Builder().setDescription(Game.gson.toJson(new ProfileMetadata(profile))).build());

                  Log.debug(Log.TAG, "saved profile " + profile.id + " (" + data.length + " bytes)");

                  return null;
                }

                case GamesStatusCodes.STATUS_SNAPSHOT_CONFLICT:
                  Snapshot snapshot = osr.getSnapshot(), conflict = osr.getConflictingSnapshot(),
                           resolved = snapshot.getMetadata().getLastModifiedTimestamp() < conflict.getMetadata().getLastModifiedTimestamp() ? conflict : snapshot;

                  Log.debug(Log.TAG, "STATUS_SNAPSHOT_CONFLICT");

                  osr = Games.Snapshots.resolveConflict(gac, osr.getConflictId(), resolved).await(30, TimeUnit.SECONDS);

                  Log.debug(Log.TAG, "resolve conflict status: " + osr.getStatus());

                  break;
              }
            }
            catch (Exception e)
            {
              Log.error(Log.TAG, e);
            }

          Log.debug(Log.TAG, "cloud save has exceeded the retries limit");

          return null;
        }
      }.execute();
    }
    else
      Log.debug(Log.TAG, "not connected to gms - no save");
  }

  public void delete(final String id)
  {
    if (isLoggedIn())
    {
      Log.debug(Log.TAG, "opening cloud game snapshot " + id);

      new AsyncTask<Void, Void, List<Profile>>()
      {
        @Override
        protected List<Profile> doInBackground(Void... voids)
        {
          try
          {
            Snapshots.OpenSnapshotResult osr = Games.Snapshots.open(gac, id, false).await();

            switch (osr.getStatus().getStatusCode())
            {
              case GamesStatusCodes.STATUS_OK:
                delete(osr.getSnapshot().getMetadata());
                break;

              case GamesStatusCodes.STATUS_SNAPSHOT_NOT_FOUND:
                Log.debug(Log.TAG, "could not open snapshot " + id + " - not found");
                break;

              case GamesStatusCodes.STATUS_CLIENT_RECONNECT_REQUIRED:
                Log.debug(Log.TAG, "could not open snapshot " + id + " - reconnect required");
                break;

              default:
                Log.debug(Log.TAG, "could not open snapshot " + id + " - internal error");
            }
          }
          catch (Exception e)
          {
            Log.error(Log.TAG, e);
          }

          return null;
        }
      }.execute();
    }
    else
      Log.debug(Log.TAG, "not connected to gms - no delete");
  }

  public void onActivityResult(Activity activity, Listener.Login listener, int result)
  {
    this.activity = activity;
    this.listeners.login = listener;

    switch (result)
    {
      case Activity.RESULT_OK:
        Log.debug(Log.TAG, "onActivityResult OK");

        gac.connect();
        break;

      case GamesActivityResultCodes.RESULT_RECONNECT_REQUIRED:
        Log.debug(Log.TAG, "onActivityResult reconnect required");

        gac.connect();
        break;

      case GamesActivityResultCodes.RESULT_SIGN_IN_FAILED:
        Log.debug(Log.TAG, "onActivityResult sign in failed");

        listener.onLoginFail();
        break;

      case Activity.RESULT_CANCELED:
        Log.debug(Log.TAG, "onActivityResult cancel");

        gac.disconnect();
        break;

      default:
        Log.debug(Log.TAG, "Unexpected result code: " + result);

        listener.onLoginFail();
    }
  }

  public boolean canLogout()
  {
    Profile p = ProfileManager.instance(context).getCurrentProfile();

    return p == null || p.settings.cloudSave == Settings.CloudSave.NONE;
  }

  public void logout()
  {
    if (isLoggedIn())
    {
      Log.debug(Log.TAG, "logged out the user from Google cloud");

      Games.signOut(gac);
      gac.disconnect();
    }
    else
      Log.debug(Log.TAG, "the user is not logged in Google cloud");
  }

  private static final class ProfileMetadata
  {
    public String name;
    public long version;

    // default constructor for gson
    public ProfileMetadata() {}
    public ProfileMetadata(Profile p)
    {
      this.name = p.nickname;
      this.version = p.version;
    }
  }

  private CloudManager(Context context)
  {
    this.context = context.getApplicationContext();

    if (GooglePlayServicesUtil.isGooglePlayServicesAvailable(this.context) == 0)
      gac = new GoogleApiClient.Builder(context)
          .addApi(Drive.API)
          .addScope(Drive.SCOPE_APPFOLDER)
          .addApi(Games.API)
          .addScope(Games.SCOPE_GAMES)
          .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks()
          {
            @Override
            public void onConnected(Bundle bundle)
            {
              Log.debug(Log.TAG, "connected to gms");

              listeners.login.onLoginSuccess();
              clear();
            }

            @Override
            public void onConnectionSuspended(int reason)
            {
              Log.debug(Log.TAG, "connection with gms suspended, reason: " + reason);

              gac.disconnect();
              listeners.login.onLoginFail();
            }
          })
          .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener()
          {
            @Override
            public void onConnectionFailed(ConnectionResult result)
            {
              if (result.hasResolution())
              {
                Log.debug(Log.TAG, "resolution required " + result);

                try
                {
                  result.startResolutionForResult(activity, REQUEST_CODE);

                  return;
                }
                catch (IntentSender.SendIntentException e)
                {
                  Log.error(Log.TAG, e);

                  gac.connect();
                }
              }
              else
                Log.debug(Log.TAG, "connection to gms failed without resolution " + result);

              listeners.login.onLoginFail();

              clear();
              gac.disconnect();
            }
          })
          .build();
  }

  private void clear()
  {
    activity = null;
    listeners.login = null;
  }

  private List<Profile> load(SnapshotMetadataBuffer smdb)
  {
    List<Profile> profiles = new LinkedList<Profile>();

    Log.debug(Log.TAG, "detected " + smdb.getCount() + " snapshots");

    for (int i = 0; i < smdb.getCount(); ++i)
    {
      SnapshotMetadata sm = smdb.get(i);

      try
      {
        ProfileMetadata pmd = Game.gson.fromJson(sm.getDescription(), ProfileMetadata.class);
        String id = sm.getUniqueName();
        Profile p = ProfileManager.instance(context).find(id);

        if (pmd != null)
        {
          Log.debug(Log.TAG, "#" + i + " name [" + pmd.name + "], id [" + id + "], snapshot [" + sm.getSnapshotId() + "]");

          // we either don't have such a profile or it's version is higher than the local one
          if (p == null || pmd.version > p.version)
          {
            try
            {
              Snapshots.OpenSnapshotResult osr = Games.Snapshots.open(gac, id, true).await(30, TimeUnit.SECONDS);

              switch (osr.getStatus().getStatusCode())
              {
                case GamesStatusCodes.STATUS_OK:
                  byte[] data = osr.getSnapshot().readFully();

                  try
                  {
                    Profile profile = Game.gson.fromJson(new String(data), Profile.class);

                    Log.debug(Log.TAG, "loaded " + profile.nickname + " (" + data.length + " bytes)");

                    profiles.add(profile);
                  }
                  catch (Exception e)
                  {
                    Log.error(Log.TAG, e);

                    // snapshot corrupted - delete it
                    delete(sm);
                  }

                  break;

                default:
                  Log.debug(Log.TAG, "error loading snapshot, status " + osr.getStatus());
              }
            }
            catch (Exception e)
            {
              Log.error(Log.TAG, e);
            }
          }
          else
            Log.debug(Log.TAG, "skipped #" + i + " name [" + id + "], id [" + sm.getSnapshotId() + "]");
        }
        else
          Log.debug(Log.TAG, "skipped #" + i + " - pmd is null");
      }
      catch (Exception e)
      {
        Log.error(Log.TAG, e);

        // metadata corrupted - delete this snapshot
        delete(sm);
      }
    }

    // close the buffer to prevent data leak
    smdb.close();

    return profiles;
  }

  private void delete(SnapshotMetadata sm)
  {
    Log.debug(Log.TAG, "deleting snapshot " + sm.getUniqueName());

    try
    {
      Snapshots.DeleteSnapshotResult dsr = Games.Snapshots.delete(gac, sm).await(30, TimeUnit.SECONDS);

      switch (dsr.getStatus().getStatusCode())
      {
        case GamesStatusCodes.STATUS_OK:
          Log.debug(Log.TAG, "snapshot " + sm.getUniqueName() + " deleted successfully");
          break;

        case GamesStatusCodes.STATUS_SNAPSHOT_NOT_FOUND:
          Log.debug(Log.TAG, "could not delete snapshot " + sm.getUniqueName() + " - not found");
          break;

        case GamesStatusCodes.STATUS_CLIENT_RECONNECT_REQUIRED:
          Log.debug(Log.TAG, "could not delete snapshot " + sm.getUniqueName() + " - reconnect required");
          break;

        default:
          Log.debug(Log.TAG, "could not delete snapshot " + sm.getUniqueName() + " - internal error");
      }
    }
    catch (Exception e)
    {
      Log.error(Log.TAG, e);
    }
  }

  private static CloudManager instance;

  // context
  private final Context context;
  // client
  private GoogleApiClient gac;
  // current activity
  private Activity activity;
  // listeners
  private Listener listeners = new Listener();
}
